/*
Copyright (C) 2011 The University of Michigan

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Please send inquiries to powertutor@umich.edu
 */

package vn.cybersoft.obs.andriod.batterystats2.service;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

import vn.cybersoft.obs.andriod.batterystats2.components.OLED;
import vn.cybersoft.obs.andriod.batterystats2.components.PowerComponent;
import vn.cybersoft.obs.andriod.batterystats2.phone.PhoneConstants;
import vn.cybersoft.obs.andriod.batterystats2.phone.PhoneSelector;
import vn.cybersoft.obs.andriod.batterystats2.phone.PowerFunction;
import vn.cybersoft.obs.andriod.batterystats2.util.Counter;
import vn.cybersoft.obs.andriod.batterystats2.util.HistoryBuffer;
import vn.cybersoft.obs.andriod.batterystats2.util.NotificationService;
import vn.cybersoft.obs.andriod.batterystats2.util.SystemInfo;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;
import android.util.SparseArray;

/** This class is responsible for starting the individual power component
 *  loggers (CPU, GPS, etc...) and collecting the information they generate.
 *  This information is used both to write a log file that will be send back
 *  to spidermoneky (or looked at by the user) and to implement the
 *  ICounterService IPC interface.
 */
public class PowerEstimator implements Runnable {
	private static final String TAG = "PowerEstimator";

	/* A dictionary used to assist in compression of the log files.  Strings that
	 * appear more frequently should be put towards the end of the dictionary. It
	 * is not critical that every string that be written to the log appear here.
	 */
	private static final String DEFLATE_DICTIONARY =
			"onoffidleoff-hookringinglowairplane-modebatteryedgeGPRS3Gunknown" +
					"in-serviceemergency-onlyout-of-servicepower-offdisconnectedconnecting" +
					"associateconnectedsuspendedphone-callservicenetworkbegin.0123456789" +
					"GPSAudioWifi3GLCDCPU-power ";

	public static final int ALL_COMPONENTS = -1;
	public static final int ITERATION_INTERVAL = 1000; // 1 second

	private UMLoggerService context;
	private SharedPreferences prefs;
	private boolean plugged;

	private Vector<PowerComponent> powerComponents;
	private Vector<PowerFunction> powerFunctions;
	private Vector<HistoryBuffer> histories;
	private Map<Integer, String> uidAppIds;

	// Miscellaneous data.
	private HistoryBuffer oledScoreHistory;

	private Object fileWriteLock = new Object();
	private LogUploader logUploader;
	private OutputStreamWriter logStream;
	private DeflaterOutputStream deflateStream;

	private Object iterationLock = new Object();
	private long lastWrittenIteration;

	public PowerEstimator(UMLoggerService context){
		this.context = context;
		prefs = PreferenceManager.getDefaultSharedPreferences(context);
		powerComponents = new Vector<PowerComponent>();
		powerFunctions = new Vector<PowerFunction>();
		uidAppIds = new HashMap<Integer, String>();
		PhoneSelector.generateComponents(context, powerComponents, powerFunctions);

		histories = new Vector<HistoryBuffer>();
		for(int i = 0; i < powerComponents.size(); i++) {
			histories.add(new HistoryBuffer(300));
		}
		oledScoreHistory = new HistoryBuffer(0);

		logUploader = new LogUploader(context);
		openLog(true);
	}

	private void openLog(boolean init) {
		/* Open up the log file if possible. */
		try {
			String logFilename = context.getFileStreamPath(
					"PowerTrace.log").getAbsolutePath();
			if(init && prefs.getBoolean("sendPermission", true) &&
					new File(logFilename).length() > 0) {
				/* There is data to send.  Make sure that gets going in the sending
				 * process before we write over any old logs.
				 */
				logUploader.upload(logFilename);
			}
			Deflater deflater = new Deflater();
			deflater.setDictionary(DEFLATE_DICTIONARY.getBytes());
			deflateStream = new DeflaterOutputStream(
					new FileOutputStream(logFilename));
			logStream = new OutputStreamWriter(deflateStream);
		} catch(IOException e) {
			logStream = null;
			Log.e(TAG, "Failed to open log file.  No log will be kept.");
		}    
	}

	/** This is the loop that keeps updating the power profile
	 */
	public void run() {
		SystemInfo sysInfo = SystemInfo.getInstance();
		PackageManager pm = context.getPackageManager();
		BatteryStats bst = BatteryStats.getInstance();

		int components = powerComponents.size();
		long beginTime = SystemClock.elapsedRealtime();
		for(int i = 0; i < components; i++) {
			powerComponents.get(i).init(beginTime, ITERATION_INTERVAL);
			powerComponents.get(i).start();
		}
		IterationData[] dataTemp = new IterationData[components];

		PhoneConstants phoneConstants = PhoneSelector.getConstants(context);
		long[] memInfo = new long[4];

		int oledId = -1;
		for(int i = 0; i < components; i++) {
			if("OLED".equals(powerComponents.get(i).getComponentName())) {
				oledId = i;
				break;
			}
		}

		double lastCurrent = -1;

		/* Indefinitely collect data on each of the power components. */
		boolean firstLogIteration = true;
		for(long iter = -1; !Thread.interrupted(); ) {
			long curTime = SystemClock.elapsedRealtime();
			/* Compute the next iteration that we can make the ending of.  We wait
         for the end of the iteration so that the components had a chance to
         collect data already.
			 */
			iter = (long)Math.max(iter + 1,
					(curTime - beginTime) / ITERATION_INTERVAL);
			/* Sleep until the next iteration completes. */
			try {
				Thread.currentThread().sleep(
						beginTime + (iter + 1) * ITERATION_INTERVAL - curTime);
			} catch(InterruptedException e) {
				break;
			}

			int totalPower = 0;
			for(int i = 0; i < components; i++) {
				PowerComponent comp = powerComponents.get(i);
				IterationData data = comp.getData(iter);
				dataTemp[i] = data;
				if(data == null) {
					/* No data present for this timestamp.  No power charged.
					 */
					continue;
				}

				SparseArray<PowerData> uidPower = data.getUidPowerData();
				for(int j = 0; j < uidPower.size(); j++) {
					int uid = uidPower.keyAt(j);
					PowerData powerData = uidPower.valueAt(j);
					int power = (int)powerFunctions.get(i).calculate(powerData);
					powerData.setCachedPower(power);
					histories.get(i).add(uid, iter, power);
					if(uid == SystemInfo.AID_ALL) {
						totalPower += power;
					}
					if(i == oledId) {
						OLED.OledData oledData = (OLED.OledData)powerData;
						if(oledData.pixPower >= 0) {
							oledScoreHistory.add(uid, iter, (int)(1000 * oledData.pixPower));
						}
					}
				}
			}

			/* Update the uid set. */
			synchronized(fileWriteLock) { synchronized(uidAppIds) {
				for(int i = 0; i < components; i++) {
					IterationData data = dataTemp[i];
					if(data == null) {
						continue;
					}
					SparseArray<PowerData> uidPower = data.getUidPowerData();
					for(int j = 0; j < uidPower.size(); j++) {
						int uid = uidPower.keyAt(j);
						if(uid < SystemInfo.AID_APP) {
							uidAppIds.put(uid, null);
						} else  {
							/* We only want to update app names when logging so the associcate
							 * message gets written.
							 */
							String appId = uidAppIds.get(uid);
							String newAppId = sysInfo.getAppId(uid, pm);
							if(!firstLogIteration && logStream != null &&
									(appId == null || !appId.equals(newAppId))) {
								try {
									logStream.write("associate " + uid + " " + newAppId + "\n");
								} catch(IOException e) {
									Log.w(TAG, "Failed to write to log file");
								}
							}
							uidAppIds.put(uid, newAppId);
						}
					}
				}
			}}

			synchronized(iterationLock) {
				lastWrittenIteration = iter;
			}

			/* Update the icon display every 15 iterations. */
			if(iter % 15 == 14) {
				final double POLY_WEIGHT = 0.02;
				int count = 0;
				int[] history = getComponentHistory(5 * 60, -1,
						SystemInfo.AID_ALL, -1);
				double weightedAvgPower = 0;
				for(int i = history.length - 1; i >= 0; i--) {
					if(history[i] != 0) {
						count++;
						weightedAvgPower *= 1.0 - POLY_WEIGHT;
						weightedAvgPower += POLY_WEIGHT * history[i] / 1000.0;
					}
				}
				double avgPower = -1;
				if(count != 0) {
					avgPower = weightedAvgPower /
							(1.0 - Math.pow(1.0 - POLY_WEIGHT, count));
				}
				avgPower *= 1000;

				/*context.updateNotification((int)Math.min(8, 1 +
                                   8 * avgPower / phoneConstants.maxPower()),
                                   avgPower);*/
			}

			/* Update the widget. */
			if(iter % 60 == 0) {
				//PowerWidget.updateWidget(context, this);
			}

			if(bst.hasCurrent()) {
				double current = bst.getCurrent();
				if(current != lastCurrent) {
					writeToLog("batt_current " + current + "\n");
					lastCurrent = current;
				}
			}
			if(iter % (5*60) == 0) {
				if(bst.hasTemp()) {
					writeToLog("batt_temp " + bst.getTemp() + "\n");
				}
				if(bst.hasCharge()) {
					writeToLog("batt_charge " + bst.getCharge() + "\n");
				}
			}
			if(iter % (30*60) == 0) {
				if(Settings.System.getInt(context.getContentResolver(),
						"screen_brightness_mode", 0) != 0) {
					writeToLog("setting_brightness automatic\n");
				} else {
					int brightness = Settings.System.getInt(
							context.getContentResolver(),
							Settings.System.SCREEN_BRIGHTNESS, -1);
					if(brightness != -1) {
						writeToLog("setting_brightness " + brightness + "\n");
					}
				}
				int timeout = Settings.System.getInt(
						context.getContentResolver(),
						Settings.System.SCREEN_OFF_TIMEOUT, -1);
				if(timeout != -1) {
					writeToLog("setting_screen_timeout " + timeout + "\n");
				}
				String httpProxy = Settings.Secure.getString(
						context.getContentResolver(),
						Settings.Secure.HTTP_PROXY);
				if(httpProxy != null) {
					writeToLog("setting_httpproxy " + httpProxy + "\n");
				}
			}

			/* Let's only grab memory information every 10 seconds to try to keep log
			 * file size down and the notice_data table size down.
			 */
			boolean hasMem = false;
			if(iter % 10 == 0) {
				hasMem = sysInfo.getMemInfo(memInfo);
			}

			synchronized(fileWriteLock) {
				if(logStream != null) try {
					if(firstLogIteration) {
						firstLogIteration = false;
						logStream.write("time " + System.currentTimeMillis() + "\n");
						Calendar cal = new GregorianCalendar();
						logStream.write("localtime_offset " +
								(cal.get(Calendar.ZONE_OFFSET) +
										cal.get(Calendar.DST_OFFSET)) + "\n");
						logStream.write("model " + phoneConstants.modelName() + "\n");
						if(NotificationService.available()) {
							logStream.write("notifications-active\n");
						}
						if(bst.hasFullCapacity()) {
							logStream.write("batt_full_capacity " + bst.getFullCapacity()
									+ "\n");
						}
						synchronized(uidAppIds) {
							for(int uid : uidAppIds.keySet()) {
								if(uid < SystemInfo.AID_APP) {
									continue;
								}
								logStream.write("associate " + uid + " " + uidAppIds.get(uid)
										+ "\n");
							}
						}
					}
					logStream.write("begin " + iter + "\n");
					logStream.write("total-power " + (long)Math.round(totalPower) + '\n');
					if(hasMem) {
						logStream.write("meminfo " + memInfo[0] + " " + memInfo[1] +
								" " + memInfo[2] + " " + memInfo[3] + "\n");
					}
					for(int i = 0; i < components; i++) {
						IterationData data = dataTemp[i];
						if(data != null) {
							String name = powerComponents.get(i).getComponentName();
							SparseArray<PowerData> uidData = data.getUidPowerData();
							for(int j = 0; j < uidData.size(); j++) {
								int uid = uidData.keyAt(j);
								PowerData powerData = uidData.valueAt(j);
								if(uid == SystemInfo.AID_ALL) {
									logStream.write(name + " " + (long)Math.round(
											powerData.getCachedPower()) + "\n");
									powerData.writeLogDataInfo(logStream);
								} else {
									logStream.write(name + "-" + uid + " " + (long)Math.round(
											powerData.getCachedPower()) + "\n");
								}
							}
							data.recycle();
						}
					}
				} catch(IOException e) {
					Log.w(TAG, "Failed to write to log file");
				}

				if(iter % 15 == 0 && prefs.getBoolean("sendPermission", true)) {
					/* Allow for LogUploader to decide if the log needs to be uploaded and
					 * begin uploading if it decides it's necessary.
					 */
					if(logUploader.shouldUpload()) {
						try {
							logStream.close();
						} catch(IOException e) {
							Log.w(TAG, "Failed to flush and close log stream");
						}
						logStream = null;
						logUploader.upload(context.getFileStreamPath(
								"PowerTrace.log").getAbsolutePath());
						openLog(false);
						firstLogIteration = true;
					}
				}
			}
		}

		/* Blank the widget's display and turn off power button. */
		//PowerWidget.updateWidgetDone(context);

		/* Have all of the power component threads exit. */
		logUploader.interrupt();
		for(int i = 0; i < components; i++) {
			powerComponents.get(i).interrupt();
		}
		try {
			logUploader.join();
		} catch(InterruptedException e) {
		}
		for(int i = 0; i < components; i++) {
			try {
				powerComponents.get(i).join();
			} catch(InterruptedException e) {
			}
		}

		/* Close the logstream so that everything gets flushed and written to file
		 * before we have to quit.
		 */
		synchronized(fileWriteLock) {
			if(logStream != null) try {
				logStream.close();
			} catch(IOException e) {
				Log.w(TAG, "Failed to flush log file on exit");
			}
		}
	}

	public void plug(boolean plugged) {
		logUploader.plug(plugged);
	}

	public void writeToLog(String m) {
		synchronized(fileWriteLock) {
			if(logStream != null) try {
				logStream.write(m);
			} catch(IOException e) {
				Log.w(TAG, "Failed to write message to power log");
			}
		}
	}

	public String[] getComponents() {
		int components = powerComponents.size();
		String[] ret = new String[components];
		for(int i = 0; i < components; i++) {
			ret[i] = powerComponents.get(i).getComponentName();
		}
		return ret;
	}

	public int[] getComponentsMaxPower() {
		PhoneConstants constants = PhoneSelector.getConstants(context);
		int components = powerComponents.size();
		int[] ret = new int[components];
		for(int i = 0; i < components; i++) {
			ret[i] = (int)constants.getMaxPower(
					powerComponents.get(i).getComponentName());
		}
		return ret;
	}

	public int getNoUidMask() {
		int components = powerComponents.size();
		int ret = 0;
		for(int i = 0; i < components; i++) {
			if(!powerComponents.get(i).hasUidInformation()) {
				ret |= 1 << i;
			}
		}
		return ret;
	}

	public int[] getComponentHistory(int count, int componentId, int uid,
			long iteration) {
		if(iteration == -1) synchronized(iterationLock) {
			iteration = lastWrittenIteration;
		}
		int components = powerComponents.size();
		if(componentId == ALL_COMPONENTS) {
			int[] result = new int[count];
			for(int i = 0; i < components; i++) {
				int[] comp = histories.get(i).get(uid, iteration, count);
				for(int j = 0; j < count; j++) {
					result[j] += comp[j];
				}
			}
			return result;
		}
		if(componentId < 0 || components <= componentId) return null;
		return histories.get(componentId).get(uid, iteration, count);
	}

	public long[] getTotals(int uid, int windowType) {
		int components = powerComponents.size();
		long[] ret = new long[components];
		for(int i = 0; i < components; i++) {
			ret[i] = histories.get(i).getTotal(uid, windowType) *
					ITERATION_INTERVAL / 1000;
		}
		return ret;
	}

	public long getRuntime(int uid, int windowType) {
		long runningTime = 0;
		int components = powerComponents.size();
		for(int i = 0; i < components; i++) {
			long entries = histories.get(i).getCount(uid, windowType);
			runningTime = entries > runningTime ? entries : runningTime;
		}
		return runningTime * ITERATION_INTERVAL / 1000;
	}

	public long[] getMeans(int uid, int windowType) {
		long[] ret = getTotals(uid, windowType);
		long runningTime = getRuntime(uid, windowType);
		runningTime = runningTime == 0 ? 1 : runningTime;
		for(int i = 0; i < ret.length; i++) {
			ret[i] /= runningTime;
		}
		return ret;
	}

	public UidInfo[] getUidInfo(int windowType, int ignoreMask) {
		long iteration;
		synchronized(iterationLock) {
			iteration = lastWrittenIteration;
		}
		int components = powerComponents.size();
		synchronized(uidAppIds) {
			int pos = 0;
			UidInfo[] result = new UidInfo[uidAppIds.size()];
			for(Integer uid : uidAppIds.keySet()) {
				UidInfo info = UidInfo.obtain();
				int currentPower = 0;
				for(int i = 0; i < components; i++) {
					if((ignoreMask & 1 << i) == 0) {
						currentPower += histories.get(i).get(uid, iteration, 1)[0];
					}
				}
				double scale = ITERATION_INTERVAL / 1000.0;
				info.init(uid, currentPower,
						sumArray(getTotals(uid, windowType), ignoreMask) *
						ITERATION_INTERVAL / 1000,
						getRuntime(uid, windowType) * ITERATION_INTERVAL / 1000);
				result[pos++] = info;
			}
			return result;
		}
	}

	private long sumArray(long[] A, int ignoreMask) {
		long ret = 0;
		for(int i = 0; i < A.length; i++) {
			if((ignoreMask & 1 << i) == 0) {
				ret += A[i];
			}
		}
		return ret;
	}

	public long getUidExtra(String name, int uid) {
		if("OLEDSCORE".equals(name)) {
			long entries = oledScoreHistory.getCount(uid, Counter.WINDOW_TOTAL);
			if(entries <= 0) return -2;
			double result = oledScoreHistory.getTotal(uid, Counter.WINDOW_TOTAL) /
					1000.0;
			result /= entries;
			PhoneConstants phoneConstants = PhoneSelector.getConstants(context);
			result *= 255 / (phoneConstants.getMaxPower("OLED") -
					phoneConstants.oledBasePower());
			return (long)Math.round(result * 100);
		}
		return -1;
	}
}

